'''
Created on 3 Dec 2012

@author: quirino.brizi
'''
from .enums import Options, Method
from .imports import pycurl, StringIO
from .messages import Future
from exceptions import CurlRequestException
from imports import urlencode, urlparse, urlunparse
from messages import Response
from threading import Thread
from types import TupleType, ListType

CURL_INFO = {
        # timers
        # An overview of the six time values available from curl_easy_getinfo()
        # perform() --> NAMELOOKUP --> CONNECT --> APPCONNECT
        # --> PRETRANSFER --> STARTTRANSFER --> TOTAL --> REDIRECT
        "TOTAL_TIME": pycurl.TOTAL_TIME,
        "NAMELOOKUP_TIME": pycurl.NAMELOOKUP_TIME,
        "CONNECT_TIME": pycurl.CONNECT_TIME,
        "APPCONNECT_TIME": pycurl.APPCONNECT_TIME,
        "PRETRANSFER_TIME": pycurl.PRETRANSFER_TIME,
        "STARTTRANSFER_TIME": pycurl.STARTTRANSFER_TIME,
        "REDIRECT_TIME": pycurl.REDIRECT_TIME,
        "HTTP_CODE": pycurl.HTTP_CODE,
        "REDIRECT_COUNT": pycurl.REDIRECT_COUNT,
        "REDIRECT_URL": pycurl.REDIRECT_URL,
        "SIZE_UPLOAD": pycurl.SIZE_UPLOAD,
        "SIZE_DOWNLOAD": pycurl.SIZE_DOWNLOAD,
        "SPEED_DOWNLOAD": pycurl.SPEED_DOWNLOAD,
        "SPEED_UPLOAD": pycurl.SPEED_UPLOAD,
        "HEADER_SIZE": pycurl.HEADER_SIZE,
        "REQUEST_SIZE": pycurl.REQUEST_SIZE,
        "SSL_VERIFYRESULT": pycurl.SSL_VERIFYRESULT,
        "SSL_ENGINES": pycurl.SSL_ENGINES,
        "CONTENT_LENGTH_DOWNLOAD": pycurl.CONTENT_LENGTH_DOWNLOAD,
        "CONTENT_LENGTH_UPLOAD": pycurl.CONTENT_LENGTH_UPLOAD,
        "CONTENT_TYPE": pycurl.CONTENT_TYPE,
    
        "HTTPAUTH_AVAIL": pycurl.HTTPAUTH_AVAIL,
        "PROXYAUTH_AVAIL": pycurl.PROXYAUTH_AVAIL,
        "OS_ERRNO": pycurl.OS_ERRNO,
        "NUM_CONNECTS": pycurl.NUM_CONNECTS,
        "PRIMARY_IP": pycurl.PRIMARY_IP,
        "CURLINFO_LASTSOCKET": pycurl.LASTSOCKET,
        "EFFECTIVE_URL": pycurl.EFFECTIVE_URL,
        "INFO_COOKIELIST": pycurl.INFO_COOKIELIST,
        "RESPONSE_CODE": pycurl.RESPONSE_CODE,
        "HTTP_CONNECTCODE": pycurl.HTTP_CONNECTCODE
        
        # "FILETIME": pycurl.FILETIME
        # "PRIVATE": pycurl.PRIVATE, # (Added in 7.10.3)
        # "CERTINFO": pycurl.CERTINFO,
        # "PRIMARY_PORT": pycurl.PRIMARY_PORT,
    }

class Connection(object):
    '''
    Connection class implementation
    '''
    def __new__(self):
        """
        Builds up the Connection class adding the pycurl.Curl()
        built-in type methods
        """
        # create the Connection instance
        obj = super(Connection, self).__new__(self)
        # create pycurl object and register its methods on this object
        curl = pycurl.Curl()   
        obj.close = curl.close
        obj.errstr = curl.errstr
        obj.getinfo = curl.getinfo
        obj.perform = curl.perform
        obj.option = curl.setopt
        obj.unsetopt = curl.unsetopt
        obj.reset = curl.reset
        obj.handle = curl
        return obj

    def __init__(self):
        '''
        Initialises a new connection class
        '''
        self.hdrcollector = None
        self.bodycollector = None
        self.fullurl = ""
    
    def __del__(self):
        """
        Release the resources hold by the this object
        """
        self.close()
    
    def exchange(self, request=None):
        """
        Sends the request to the replier waiting for its response.
        @param request: the request message
        @return: the Response message 
        """
        self.prepare(request)
        try :
            self.perform()
        except pycurl.error, e:
            raise CurlRequestException(e[0], e[1])
        else :
            return self.response()
    
    def response(self):
        return Response(self.fullurl, self.hdrcollector, self.bodycollector, 
                            self._info())
        
    def prepare(self, request):
        
        self.fullurl = self._full_url(request.get_url(), request.get_qs())
        method = request.get_method()
        self.option(pycurl.URL, self.fullurl)
        self.option(method, True)
        self.option(pycurl.NOSIGNAL, True)
        self.option(pycurl.HTTPHEADER, request.get_headers())
        
        options = request.get_options()
        for k,v in options.iteritems():
            if v is not None : self.option(k,v)
        
        self.option(Options.VERBOSE, 
                True if Options.DEBUGFUNCTION in options else False)
        if not Options.HEADERFUNCTION in options:
            self.hdrcollector = StringIO()
            self.option(Options.HEADERFUNCTION, self.hdrcollector.write)
        if not Options.HEADERFUNCTION in options:
            self.bodycollector = StringIO()
            self.option(Options.BODYFUNCTION, self.bodycollector.write)
        
        if request.get_security() is not None:
            request.get_security().configure(self)
        if request.get_authentication() is not None:
            request.get_authentication().configure(self)
        
        # get options from pycurl instead of Option here in order to
        # try hiding the post fields from outside and force using request
        if method in (Method.POST, Method.PUT):
            files = request.get_files()
            data = request.get_data()
            if len(files) > 0 :
                if data is not None and isinstance(data, (TupleType, ListType)) : 
                    files.extend(data)
                self.option(pycurl.HTTPPOST, files)
            elif data is not None :
                size = len(data)
                opt = pycurl.INFILESIZE if method is Method.PUT else pycurl.POSTFIELDSIZE
                self.option(opt, size)
                self.option(pycurl.POSTFIELDS, data)
            else :
                raise CurlRequestException(99, 
                    "Invalid configuration for POST/PUT neither data nor files given")
    
    # Private methods
    def _encode_params(self, params={}):
        """
        Encode the dictionary parameters into a valid query string
        @param params: the query string parameters as dict
        """
        ep = []
        append = ep.append
        tp = (ListType, TupleType)
        for k,v in params.iteritems() :
            if isinstance(v, tp) : 
                for d in v : append((k,d))
            else : 
                append((k,str(v)))
        return urlencode(ep)
                    
    def _full_url(self, url, qsparams={}):
        
        scheme, netloc, path, params, query, fragment = urlparse(url)
        try :
            netloc.encode('idna').decode('utf-8')
        except :
            raise ValueError("Invalid URL %s" % url)
        
        #if not scheme : scheme = "http"
        eparams = self._encode_params(qsparams)
        if not query:
            query = eparams
        else:
            if len(eparams) > 0: query = "&".join([query, eparams])
        return urlunparse([scheme, netloc, path, params, query, fragment])
    
    def _info(self):
        """
        Collect the detailed transaction information from the cURL handler
        @param conn: the cURL handler
        """
        info = dict()
        for k,v in CURL_INFO.iteritems() : info[k] = self.getinfo(v)
        return info

class AsyncHTTPConnection(object):
    """
    Asynchronous HTTP connection handler. Executes asynchronously a set of 
    HTTP connection asynchronously, results of the request-response 
    communication are stored at retrieval time on the Future object returned 
    by the exchange method.
    """
    def __new__(self, connections=[]):
        # create the Connection instance
        obj = super(AsyncHTTPConnection, self).__new__(self)
        multi = pycurl.CurlMulti()
        obj.add = multi.add_handle
        obj.perform = multi.perform
        obj.close = multi.close
        obj.remove = multi.remove_handle
        obj.fdset = multi.fdset
        obj.select = multi.select
        obj.info = multi.info_read
        return obj
    
        
    def __init__(self, connections=[]):
        """
        Initialises the asynchronous connection
        @param connections: the list of prepared connection to use for 
        querying the remote server.
        """
        self.done = False
        self.connections = {}
        self.futures = []
        for conn in connections:
            handle = conn.handle
            self.add(handle)
            self.futures.append(Future())
            self.connections[handle] = conn
    
    def exchange(self):
        """
        Issues the registered requests to the replier asynchronously
        @return: a list of Future objects that will be updated as soon that 
        the response from the server is ready.
        """
        thread = Thread(target=self._worker, name="AsyncHTTPConnection Worker")
        thread.start()
        return self.futures
    
    def isdone(self):
        """
        Exposes the AsyncHTTPConnection status.
        @return: True if the processing is done, False otherwise
        """
        return self.done
    
    def _worker(self):
        """
        Execute the request set and prepares the futures
        """
        np = 0
        nc = len(self.connections)
        fi = 0
        try :
            while np < nc:
                while 1:
                    r, _ = self.perform()
                    nq, ok, er = self.info()
                    for conn in ok:
                        self.remove(conn)
                        wrap = self.connections[conn]
                        fut = self.futures[fi]
                        fut.set(wrap.response(), None)
                        fi += 1
                    for conn, en, em in er:
                        self.remove(conn)
                        wrap = self.connections[conn]
                        fut = self.futures[fi]
                        fut.set(None, 
                            CurlRequestException(en, em, wrap.response()))
                        fi += 1
                    np = np + len(ok) + len(er)
                    if nq == 0 and r != pycurl.E_CALL_MULTI_PERFORM: break
        finally:
            self.done = True
                
            